{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 循环神经网络的简洁实现\n",
    "\n",
    "本节将使用Gluon来更简洁地实现基于循环神经网络的语言模型。首先，我们读取周杰伦专辑歌词数据集。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "1"
    }
   },
   "outputs": [],
   "source": [
    "import d2lzh as d2l\n",
    "import math\n",
    "from mxnet import autograd, gluon, init, nd\n",
    "from mxnet.gluon import loss as gloss, nn, rnn\n",
    "import time\n",
    "\n",
    "(corpus_indices, char_to_idx, idx_to_char,\n",
    " vocab_size) = d2l.load_data_jay_lyrics()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型\n",
    "\n",
    "Gluon的`rnn`模块提供了循环神经网络的实现。下面构造一个含单隐藏层、隐藏单元个数为256的循环神经网络层`rnn_layer`，并对权重做初始化。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "26"
    }
   },
   "outputs": [],
   "source": [
    "num_hiddens = 256\n",
    "rnn_layer = rnn.RNN(num_hiddens)\n",
    "rnn_layer.initialize()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来调用`rnn_layer`的成员函数`begin_state`来返回初始化的隐藏状态列表。它有一个形状为(隐藏层个数, 批量大小, 隐藏单元个数)的元素。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "37"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(1, 2, 256)"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "batch_size = 2\n",
    "state = rnn_layer.begin_state(batch_size=batch_size)\n",
    "state[0].shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "与上一节中实现的循环神经网络不同，这里`rnn_layer`的输入形状为(时间步数, 批量大小, 输入个数)。其中输入个数即one-hot向量长度（词典大小）。此外，`rnn_layer`作为Gluon的`rnn.RNN`实例，在前向计算后会分别返回输出和隐藏状态，其中输出指的是隐藏层在各个时间步上计算并输出的隐藏状态，它们通常作为后续输出层的输入。需要强调的是，该“输出”本身并不涉及输出层计算，形状为(时间步数, 批量大小, 隐藏单元个数)。而`rnn.RNN`实例在前向计算返回的隐藏状态指的是隐藏层在最后时间步的可用于初始化下一时间步的隐藏状态：当隐藏层有多层时，每一层的隐藏状态都会记录在该变量中；对于像长短期记忆这样的循环神经网络，该变量还会包含其他信息。我们会在本章的后面介绍长短期记忆和深度循环神经网络。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "38"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "((35, 2, 256), 1, (1, 2, 256))"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "num_steps = 35\n",
    "X = nd.random.uniform(shape=(num_steps, batch_size, vocab_size))\n",
    "Y, state_new = rnn_layer(X, state)\n",
    "Y.shape, len(state_new), state_new[0].shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来我们继承`Block`类来定义一个完整的循环神经网络。它首先将输入数据使用one-hot向量表示后输入到`rnn_layer`中，然后使用全连接输出层得到输出。输出个数等于词典大小`vocab_size`。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "39"
    }
   },
   "outputs": [],
   "source": [
    "# 本类已保存在d2lzh包中方便以后使用\n",
    "class RNNModel(nn.Block):\n",
    "    def __init__(self, rnn_layer, vocab_size, **kwargs):\n",
    "        super(RNNModel, self).__init__(**kwargs)\n",
    "        self.rnn = rnn_layer\n",
    "        self.vocab_size = vocab_size\n",
    "        self.dense = nn.Dense(vocab_size)\n",
    "\n",
    "    def forward(self, inputs, state):\n",
    "        # 将输入转置成(num_steps, batch_size)后获取one-hot向量表示\n",
    "        X = nd.one_hot(inputs.T, self.vocab_size)\n",
    "        Y, state = self.rnn(X, state)\n",
    "        # 全连接层会首先将Y的形状变成(num_steps * batch_size, num_hiddens)，它的输出\n",
    "        # 形状为(num_steps * batch_size, vocab_size)\n",
    "        output = self.dense(Y.reshape((-1, Y.shape[-1])))\n",
    "        return output, state\n",
    "\n",
    "    def begin_state(self, *args, **kwargs):\n",
    "        return self.rnn.begin_state(*args, **kwargs)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练模型\n",
    "\n",
    "同上一节一样，下面定义一个预测函数。这里的实现区别在于前向计算和初始化隐藏状态的函数接口。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "41"
    }
   },
   "outputs": [],
   "source": [
    "# 本函数已保存在d2lzh包中方便以后使用\n",
    "def predict_rnn_gluon(prefix, num_chars, model, vocab_size, ctx, idx_to_char,\n",
    "                      char_to_idx):\n",
    "    # 使用model的成员函数来初始化隐藏状态\n",
    "    state = model.begin_state(batch_size=1, ctx=ctx)\n",
    "    output = [char_to_idx[prefix[0]]]\n",
    "    for t in range(num_chars + len(prefix) - 1):\n",
    "        X = nd.array([output[-1]], ctx=ctx).reshape((1, 1))\n",
    "        (Y, state) = model(X, state)  # 前向计算不需要传入模型参数\n",
    "        if t < len(prefix) - 1:\n",
    "            output.append(char_to_idx[prefix[t + 1]])\n",
    "        else:\n",
    "            output.append(int(Y.argmax(axis=1).asscalar()))\n",
    "    return ''.join([idx_to_char[i] for i in output])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "让我们使用权重为随机值的模型来预测一次。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "42"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'分开命连翔再杨占踢泥招莫'"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ctx = d2l.try_gpu()\n",
    "model = RNNModel(rnn_layer, vocab_size)\n",
    "model.initialize(force_reinit=True, ctx=ctx)\n",
    "predict_rnn_gluon('分开', 10, model, vocab_size, ctx, idx_to_char, char_to_idx)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来实现训练函数。算法同上一节的一样，但这里只使用了相邻采样来读取数据。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "18"
    }
   },
   "outputs": [],
   "source": [
    "# 本函数已保存在d2lzh包中方便以后使用\n",
    "def train_and_predict_rnn_gluon(model, num_hiddens, vocab_size, ctx,\n",
    "                                corpus_indices, idx_to_char, char_to_idx,\n",
    "                                num_epochs, num_steps, lr, clipping_theta,\n",
    "                                batch_size, pred_period, pred_len, prefixes):\n",
    "    loss = gloss.SoftmaxCrossEntropyLoss()\n",
    "    model.initialize(ctx=ctx, force_reinit=True, init=init.Normal(0.01))\n",
    "    trainer = gluon.Trainer(model.collect_params(), 'sgd',\n",
    "                            {'learning_rate': lr, 'momentum': 0, 'wd': 0})\n",
    "\n",
    "    for epoch in range(num_epochs):\n",
    "        l_sum, n, start = 0.0, 0, time.time()\n",
    "        data_iter = d2l.data_iter_consecutive(\n",
    "            corpus_indices, batch_size, num_steps, ctx)\n",
    "        state = model.begin_state(batch_size=batch_size, ctx=ctx)\n",
    "        for X, Y in data_iter:\n",
    "            for s in state:\n",
    "                s.detach()\n",
    "            with autograd.record():\n",
    "                (output, state) = model(X, state)\n",
    "                y = Y.T.reshape((-1,))\n",
    "                l = loss(output, y).mean()\n",
    "            l.backward()\n",
    "            # 梯度裁剪\n",
    "            params = [p.data() for p in model.collect_params().values()]\n",
    "            d2l.grad_clipping(params, clipping_theta, ctx)\n",
    "            trainer.step(1)  # 因为已经误差取过均值，梯度不用再做平均\n",
    "            l_sum += l.asscalar() * y.size\n",
    "            n += y.size\n",
    "\n",
    "        if (epoch + 1) % pred_period == 0:\n",
    "            print('epoch %d, perplexity %f, time %.2f sec' % (\n",
    "                epoch + 1, math.exp(l_sum / n), time.time() - start))\n",
    "            for prefix in prefixes:\n",
    "                print(' -', predict_rnn_gluon(\n",
    "                    prefix, pred_len, model, vocab_size, ctx, idx_to_char,\n",
    "                    char_to_idx))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "使用和上一节实验中一样的超参数来训练模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "attributes": {
     "classes": [],
     "id": "",
     "n": "19"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch 50, perplexity 79.292694, time 0.04 sec\n",
      " - 分开 我不想这样  我有你这 我使用双 你天用双 我不用空 你天用双 我不用空 你天用双 我不用空 你天\n",
      " - 不分开 我不在这截棍 哼哼哈兮 我使用双截棍 哼哼哈兮 我使用双截棍 哼哼哈兮 我使用双截棍 哼哼哈兮 我\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch 100, perplexity 13.632524, time 0.04 sec\n",
      " - 分开你的美言胡藏著卷风我的那画面怎么小不 我不你这辈子注定的手不放开主爱可我想想处熬躲难我 甩开依种每日\n",
      " - 不分开 我想你的辈活在西元前爱深一在美索不达米亚平原不著我妈你的怒受女人 爱坏有可以简简单老没有伤害我想无\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch 150, perplexity 4.610021, time 0.04 sec\n",
      " - 分开你 不作我遇见你是一场悲剧 我想我这辈子注定一个不演戏 最后再一个人大早许回不想著天 这是我怕眼泪撑\n",
      " - 不分开 我是多的叹息注提的砖不放开 爱果不直以你 一直抢邻我都出出可多为我 别不盯旧语日折一枝杨柳 你在那\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch 200, perplexity 2.462100, time 0.04 sec\n",
      " - 分开 我已无黑样照着你 这静太笑 你想一定是人 塞北的客栈人多 牧草有没有 我马儿有些瘦 天涯尽头 满脸\n",
      " - 不分开 我后 的叹奏 后悔着对不  让悔着对 所在好在 他有螂容  但不确 这了彩痛 我不懂受我 我 一壶\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "epoch 250, perplexity 1.910200, time 0.04 sec\n",
      " - 分开 问养堂 最属于那年代南墙黑 担心今天飞了过狼的姑窗 夕阳斜过映在我驳天的把我 别弄依种 我跟了这节\n",
      " - 不分开 我轻轻的叹息 后悔着对不起离开 陷怪我 别怪我 难你怎么面对我 甩开球我满腔的怒火 我想揍你已经很\n"
     ]
    }
   ],
   "source": [
    "num_epochs, batch_size, lr, clipping_theta = 250, 32, 1e2, 1e-2\n",
    "pred_period, pred_len, prefixes = 50, 50, ['分开', '不分开']\n",
    "train_and_predict_rnn_gluon(model, num_hiddens, vocab_size, ctx,\n",
    "                            corpus_indices, idx_to_char, char_to_idx,\n",
    "                            num_epochs, num_steps, lr, clipping_theta,\n",
    "                            batch_size, pred_period, pred_len, prefixes)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 小结\n",
    "\n",
    "* Gluon的`rnn`模块提供了循环神经网络层的实现。\n",
    "* Gluon的`rnn.RNN`实例在前向计算后会分别返回输出和隐藏状态。该前向计算并不涉及输出层计算。\n",
    "\n",
    "## 练习\n",
    "\n",
    "* 与上一节的实现进行比较。看看Gluon的实现是不是运行速度更快？如果你觉得差别明显，试着找找原因。\n",
    "\n",
    "\n",
    "## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/4089)\n",
    "\n",
    "![](../img/qr_rnn-gluon.svg)"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}